Backtesting Dual moving average strategy on multiple assets using vectorbt¶
In this tutorial, we will talk about Multi Asset Portfolio Simulation, beginning with:
Running Multi-asset Portfolio Backtesting simulation using vbt.Portfolio.from_signals() like:
- Unified Portfolio Simulation
- Asset-wise Discrete Portfolio Simulation
- Grouped Portfolio Simulation
1) Unified Portfolio Simulation¶
import vectorbt as vbt
import numpy
import pandas
import warnings
from plotly.offline import init_notebook_mode
import plotly.io as pio
init_notebook_mode()
pio.renderers.default = 'notebook'
warnings.filterwarnings("ignore")
symbols = ["MSFT","AAPL","GOOGL"]
close_price = vbt.YFData.download(symbols, interval="1d",
missing_index="drop",
start="2020-01-01").get("Close")
print(close_price)
symbol MSFT AAPL GOOGL Date 2019-12-31 05:00:00+00:00 152.596512 71.711731 66.969498 2020-01-02 05:00:00+00:00 155.422028 73.347923 68.433998 2020-01-03 05:00:00+00:00 153.486771 72.634865 68.075996 2020-01-06 05:00:00+00:00 153.883514 73.213631 69.890503 2020-01-07 05:00:00+00:00 152.480408 72.869293 69.755501 ... ... ... ... 2023-07-31 04:00:00+00:00 335.920013 196.449997 132.720001 2023-08-01 04:00:00+00:00 336.339996 195.610001 131.550003 2023-08-02 04:00:00+00:00 327.500000 192.580002 128.380005 2023-08-03 04:00:00+00:00 326.660004 191.169998 128.449997 2023-08-04 04:00:00+00:00 327.779999 181.990005 128.110001 [905 rows x 3 columns]
"""
Setup entry and exit condition, which is with moving average (MA)
crossover combination of fast MA of 9 days and slow MA of 17 days
"""
SMA_9 = vbt.MA.run(close_price, window=9)
SMA_17 = vbt.MA.run(close_price, window=17)
entries = SMA_9.ma_crossed_above(SMA_17)
exits = SMA_9.ma_crossed_below(SMA_17)
Description of a few Parameter settings for vbt.Portfolio.from_signals()¶
We will see a short description of the new parameters of vbt.Portfolio.from_signals()
a.) size : Specifies the position size in units. For any fixed size, you can set to any number to buy/sell some fixed amount or value. For any target size, you can set to any number to buy/sell an amount relative to the current position or value. If you set this to np.nan or 0 it will get skipped (or close the current position in the case of setting 0 for any target size). Set to np.inf to buy for all cash, or -np.inf to sell for all free cash. A point to remember setting to np.inf may cause the scenario for the portfolio simulation to become heavily weighted to one single instrument. So use a sensible size related.
b.) size_type: Choose units to be used for the size. In this tutorial, we use percent and for other parameter like amount, value, TargetValue,TargetAmount, please refer here: https://vectorbt.dev/api/portfolio/enums/#vectorbt.portfolio.enums.SizeType for more explanation.
b.) init_cash : Initial capital per column (or per group with cash sharing). By setting it to auto the initial capital is automatically decided based on the position size you specify in the above size parameter.
c.) cash_sharing : Accepts a boolean (True or False) value to specify whether cash sharing is to be disabled or if enabled then cash is shared across all the assets in the portfolio or cash is shared within the same group.
If group_by is None and cash_sharing is True, group_by becomes True to form a single group with cash sharing. Example:
Consider three columns (3 assets), each having $100 of starting capital. If we built one group of two columns and one group of one column, the init_cash would be np.array([200, 100]) with cash sharing enabled and np.array([100, 100, 100]) without cash sharing.
d.) call_seq : Default sequence of calls per row and group. Controls the sequence in which order_func_nb is executed within each segment. For more details of this function kindly refer the documentation.
e.) group_by : can be boolean, integer, string, or sequence to call multi-level indexing and can accept both level names and level positions. In this tutorial I will be setting group_by = True to treat the entire portfolio simulation in a unified manner for all assets in congruence with cash_sharing = True. When I want to create custom groups with specific symbols in each group then I will be setting group_by = 0 to specify the level position (in multi-index levels) as the first in the hierarchy.``
"""In this section, we run the portfolio simulation treating the entire portfolio as a singular asset by enabling the following parameters in the pf.from_signals():
cash_sharing = True
group_by = True
call_seq = "auto"
size = 1000
"""
unified_portfolio = vbt.Portfolio.from_signals(close_price,
entries,
exits,
init_cash=100000, # in $
fees=0.0025, # in %
slippage=0.0025, # in %
freq="1D",
direction="LongOnly",
group_by=True,
cash_sharing=True,
call_seq="auto",
size_type="value",
size=10000) #
unified_portfolio.stats()
Start 2019-12-31 05:00:00+00:00 End 2023-08-04 04:00:00+00:00 Period 905 days 00:00:00 Start Value 100000.0 End Value 114244.371909 Total Return [%] 14.244372 Benchmark Return [%] 119.959258 Max Gross Exposure [%] 37.04273 Total Fees Paid 3849.043592 Max Drawdown [%] 9.599631 Max Drawdown Duration 410 days 00:00:00 Total Trades 77 Total Closed Trades 75 Total Open Trades 2 Open Trade PnL 2225.853808 Win Rate [%] 41.333333 Best Trade [%] 53.523193 Worst Trade [%] -15.414574 Avg Winning Trade [%] 10.222951 Avg Losing Trade [%] -4.477864 Avg Winning Trade Duration 32 days 02:19:21.290322580 Avg Losing Trade Duration 10 days 18:00:00 Profit Factor 1.608475 Expectancy 160.246908 Sharpe Ratio 0.832017 Calmar Ratio 0.574791 Omega Ratio 1.158445 Sortino Ratio 1.223773 Name: group, dtype: object
unified_portfolio.plot(group_by=True, subplots=["cum_returns","cash","value"]).show()
Asset-wise Discrete Portfolio Simulation¶
In this section, we will see how to run the portfolio simulation for each asset in the portfolio independently.
discrete_portfolio = vbt.Portfolio.from_signals(close_price,
entries,
exits,
init_cash=100000, # in $
fees=0.0025, # in %
slippage=0.0025, # in %
direction="LongOnly",
freq="1D",
group_by=False,
call_seq="auto",
size_type="value",
size=10000) # For each trades, limit the position size in $
# trade each assets with start value of 100,000 and apply the trading strategy respectively to see which compa
stats_df = pandas.concat([unified_portfolio.stats()] +
[discrete_portfolio[symbol].stats() for symbol
in discrete_portfolio.wrapper.columns], axis = 1)
stats_df.rename(inplace = True, columns = {'group':'unified portfolio'})
stats_df
| unified portfolio | (9, 17, MSFT) | (9, 17, AAPL) | (9, 17, GOOGL) | |
|---|---|---|---|---|
| Start | 2019-12-31 05:00:00+00:00 | 2019-12-31 05:00:00+00:00 | 2019-12-31 05:00:00+00:00 | 2019-12-31 05:00:00+00:00 |
| End | 2023-08-04 04:00:00+00:00 | 2023-08-04 04:00:00+00:00 | 2023-08-04 04:00:00+00:00 | 2023-08-04 04:00:00+00:00 |
| Period | 905 days 00:00:00 | 905 days 00:00:00 | 905 days 00:00:00 | 905 days 00:00:00 |
| Start Value | 100000.0 | 100000.0 | 100000.0 | 100000.0 |
| End Value | 114244.371909 | 100464.286929 | 109144.818863 | 104635.266117 |
| Total Return [%] | 14.244372 | 0.464287 | 9.144819 | 4.635266 |
| Benchmark Return [%] | 119.959258 | 114.801764 | 153.779965 | 91.296045 |
| Max Gross Exposure [%] | 37.04273 | 13.156697 | 15.374071 | 13.08177 |
| Total Fees Paid | 3849.043592 | 1408.18117 | 1098.078495 | 1342.783926 |
| Max Drawdown [%] | 9.599631 | 5.323669 | 2.423966 | 4.499051 |
| Max Drawdown Duration | 410 days 00:00:00 | 427 days 00:00:00 | 319 days 00:00:00 | 428 days 00:00:00 |
| Total Trades | 77 | 28 | 22 | 27 |
| Total Closed Trades | 75 | 28 | 21 | 26 |
| Total Open Trades | 2 | 0 | 1 | 1 |
| Open Trade PnL | 2225.853808 | 0.0 | 2061.436761 | 164.417047 |
| Win Rate [%] | 41.333333 | 39.285714 | 42.857143 | 42.307692 |
| Best Trade [%] | 53.523193 | 27.570619 | 53.523193 | 31.516836 |
| Worst Trade [%] | -15.414574 | -15.414574 | -9.613316 | -9.47488 |
| Avg Winning Trade [%] | 10.222951 | 7.22285 | 13.122513 | 10.850683 |
| Avg Losing Trade [%] | -4.477864 | -4.40118 | -3.953787 | -4.984034 |
| Avg Winning Trade Duration | 32 days 02:19:21.290322580 | 32 days 02:10:54.545454545 | 36 days 21:19:59.999999999 | 28 days 04:21:49.090909091 |
| Avg Losing Trade Duration | 10 days 18:00:00 | 10 days 05:38:49.411764705 | 8 days 22:00:00 | 12 days 19:12:00 |
| Profit Factor | 1.608475 | 1.061899 | 2.48923 | 1.596531 |
| Expectancy | 160.246908 | 16.581676 | 337.30391 | 171.955733 |
| Sharpe Ratio | 0.832017 | 0.083773 | 1.274723 | 0.628242 |
| Calmar Ratio | 0.574791 | 0.035125 | 1.481968 | 0.409917 |
| Omega Ratio | 1.158445 | 1.015515 | 1.263997 | 1.129641 |
| Sortino Ratio | 1.223773 | 0.117409 | 1.947054 | 0.939655 |
(Optional) Additional work - just to check trading orders & PnL of AAPL¶
discrete_portfolio[(9, 17, "AAPL")].plot().show()